package interceptcrossing;

import interceptcrossing.command.Command;
import interceptcrossing.command.CommandManager;

import java.util.*;

/**
 * @author ibotyan
 */
public class IntersectionFinder {

	/**
	 * Набор отрезков, среди которых нужно найти пересекающиеся
	 */
	private Collection<Intercept> intercepts;

	/**
	 * Результат (набор пересекающихся отрезков)
	 */
	private Set<Intersection> result;

	/**
	 * Полное упорядочение отрезков
	 */
	private PriorityQueue<Abscissa> fullOrdering;

	/**
	 * Статус заметающией прямой
	 */
	private SweptLineStatus sweptLineStatus;

	private CommandManager cmdManager;

	private List<Listener> listeners = new LinkedList<Listener>();

	public IntersectionFinder(Collection<Intercept> intercepts) {
		if (intercepts.isEmpty()) {
			throw new IllegalArgumentException("Intercept collection cannot be empty");
		}

		this.intercepts = intercepts;
		this.cmdManager = new CommandManager();
		this.result = new HashSet<Intersection>();
		this.fullOrdering = new PriorityQueue<Abscissa>();
		this.sweptLineStatus = new SweptLineStatus();

		this.eval();

		this.cmdManager.addListener(new CommandManager.Listener() {

			public void commandDone(CommandManager cmdManager, Command command) {
			}

			public void commandUndone(CommandManager cmdManager, Command command) {
				IntersectionFinder.this.fireCurrentAbscissaChanged();
			}

			public void commandRedone(CommandManager cmdManager, Command command) {
				IntersectionFinder.this.fireCurrentAbscissaChanged();
			}
		});
	}

	private class IntersectionResult {

		public final Intersection intersection;
		public final Abscissa abscissa;

		public IntersectionResult(Intersection intersection, Abscissa abscissa) {
			this.intersection = intersection;
			this.abscissa = abscissa;
		}
	}

	private IntersectionResult checkIntersection(Intercept first, Intercept second, Abscissa abscissa) {
		IntersectionResult ret = null;

		if (((first != null) && (second != null)) && (!first.equals(second))) {
			Point point = first.intersects(second);

			if (point != null) {
				if ((abscissa == null) || point.x() > abscissa.x) {
					Intersection intersection = new Intersection(first, second, point);

					if (!this.result.contains(intersection)) {
						Abscissa newAbscissa = new Abscissa(first, Bound.INTERSECTION, second);
						this.fullOrdering.add(newAbscissa);
						this.result.add(intersection);

						ret = new IntersectionResult(intersection, newAbscissa);
					}
				}
			}
		}

		return ret;
	}

	public boolean hasPreviousStep() {
		return this.cmdManager.canUndo();
	}

	public void previousStep() {
		this.cmdManager.undo();
	}

	public boolean hasNextStep() {
		return this.cmdManager.canRedo();
	}

	public void nextStep() {
		this.cmdManager.redo();
	}

	public Set<Intersection> getIntersections() {
		return this.result;
	}

	private void reset() {
		this.result.clear();
		this.sweptLineStatus.clear();
		this.fullOrdering.clear();

		for (Intercept intercept : this.intercepts) {
			this.fullOrdering.add(new Abscissa(intercept, Bound.LEFT, null));
			this.fullOrdering.add(new Abscissa(intercept, Bound.RIGHT, null));
		}
	}

	private void eval() {
		this.reset();

		while (!this.fullOrdering.isEmpty()) {
			Abscissa next = this.fullOrdering.poll();

			if (next.bound == Bound.LEFT) {
				this.cmdManager.doCommand(new LeftBoundCommand(next));
			} else if (next.bound == Bound.RIGHT) {
				this.cmdManager.doCommand(new RightBoundCommand(next));
			} else if (next.bound == Bound.INTERSECTION) {
				this.cmdManager.doCommand(new IntersectionBoundCommand(next));
			}
		}
	}

	public void addListener(Listener listener) {
		if (!this.listeners.contains(listener)) {
			this.listeners.add(listener);

			Abscissa abscissa = this.fullOrdering.peek();

			if(abscissa != null) {
				Collection<Intercept> aboveIntercepts = this.sweptLineStatus.getAllAbove(abscissa.intercept, abscissa);
				Collection<Intercept> underIntercepts = this.sweptLineStatus.getAllUnder(abscissa.intercept, abscissa);
				listener.currentAbscissaChanged(abscissa, aboveIntercepts, underIntercepts);
			}
		}
	}

	public void removeListener(Listener listener) {
		if (this.listeners.contains(listener)) {
			this.listeners.remove(listener);
		}
	}

	private void fireCurrentAbscissaChanged() {
		Abscissa abscissa = this.fullOrdering.peek();

		if(abscissa != null) {
			Collection<Intercept> aboveIntercepts = this.sweptLineStatus.getAllAbove(abscissa.intercept, abscissa);
			Collection<Intercept> underIntercepts = this.sweptLineStatus.getAllUnder(abscissa.intercept, abscissa);

			for (Listener listener : this.listeners) {
				listener.currentAbscissaChanged(abscissa, aboveIntercepts, underIntercepts);
			}
		} else {
			for (Listener listener : this.listeners) {
				listener.currentAbscissaChanged(null, null, null);
			}
		}
	}

	public static interface Listener {

		public void currentAbscissaChanged(Abscissa abscissa, Collection<Intercept> aboveIntercepts,
		                                   Collection<Intercept> underIntercepts);

		/*public void intersectionAdded(Intersection intersection);

		public void intersectionRemoved(Intersection intersection);

		public void nextAbscissaGiven(Abscissa abscissa);*/
	}

	private class LeftBoundCommand implements Command {

		private Abscissa abscissa;
		private IntersectionResult aboveIntersection;
		private IntersectionResult underIntersection;

		public LeftBoundCommand(Abscissa abscissa) {
			this.abscissa = abscissa;
		}

		public String getDescription() {
			return "Начало отрезка " + this.abscissa.intercept.toString();
		}

		public void doCommand() {
			IntersectionFinder.this.sweptLineStatus.add(this.abscissa.intercept);

			Intercept above = IntersectionFinder.this.sweptLineStatus.getAbove(this.abscissa.intercept, this.abscissa);
			Intercept under = IntersectionFinder.this.sweptLineStatus.getUnder(this.abscissa.intercept, this.abscissa);

			this.aboveIntersection = checkIntersection(above, this.abscissa.intercept, null);
			this.underIntersection = checkIntersection(this.abscissa.intercept, under, null);
			IntersectionFinder.this.fullOrdering.remove(this.abscissa);
			//IntersectionFinder.this.fireCurrentAbscissaChanged();
		}

		public void undoCommand() {
			if (this.underIntersection != null) {
				IntersectionFinder.this.result.remove(this.underIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.underIntersection.abscissa);
			}

			if (this.aboveIntersection != null) {
				IntersectionFinder.this.result.remove(this.aboveIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.aboveIntersection.abscissa);
			}

			IntersectionFinder.this.sweptLineStatus.remove(this.abscissa.intercept);
			IntersectionFinder.this.fullOrdering.add(this.abscissa);
			//IntersectionFinder.this.fireCurrentAbscissaChanged();
		}
	}

	private class RightBoundCommand implements Command {

		private Abscissa abscissa;
		private IntersectionResult aboveIntersection;

		public RightBoundCommand(Abscissa abscissa) {
			this.abscissa = abscissa;
		}

		public String getDescription() {
			return "Конец отрезка " + this.abscissa.intercept.toString();
		}

		public void doCommand() {
			Intercept above = IntersectionFinder.this.sweptLineStatus.getAbove(this.abscissa.intercept, this.abscissa);
			Intercept under = IntersectionFinder.this.sweptLineStatus.getUnder(this.abscissa.intercept, this.abscissa);
			this.aboveIntersection = checkIntersection(above, under, this.abscissa);

			IntersectionFinder.this.sweptLineStatus.remove(this.abscissa.intercept);
			IntersectionFinder.this.fullOrdering.remove(this.abscissa);
			//IntersectionFinder.this.fireCurrentAbscissaChanged();
		}

		public void undoCommand() {
			if (this.aboveIntersection != null) {
				IntersectionFinder.this.result.remove(this.aboveIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.aboveIntersection.abscissa);
			}

			IntersectionFinder.this.sweptLineStatus.add(this.abscissa.intercept);
			IntersectionFinder.this.fullOrdering.add(this.abscissa);
			//IntersectionFinder.this.fireCurrentAbscissaChanged();
		}
	}

	private class IntersectionBoundCommand implements Command {

		private Abscissa abscissa;
		private IntersectionResult aboveSecondIntersection;
		private IntersectionResult interceptUnderIntersection;
		private IntersectionResult underSecondIntersection;
		private IntersectionResult interceptAboveIntersection;

		public IntersectionBoundCommand(Abscissa abscissa) {
			this.abscissa = abscissa;
		}

		public String getDescription() {
			return "Пересечение отрезков" + this.abscissa.toString();
		}

		public void doCommand() {
			Intercept intercept = this.abscissa.intercept;
			Intercept second = this.abscissa.secondIntercept;

			Intercept above = IntersectionFinder.this.sweptLineStatus.getAbove(intercept, this.abscissa);
			Intercept under = IntersectionFinder.this.sweptLineStatus.getUnder(second, this.abscissa);

			this.aboveSecondIntersection = checkIntersection(above, second, this.abscissa);
			this.interceptUnderIntersection = checkIntersection(intercept, under, this.abscissa);
			this.underSecondIntersection = checkIntersection(under, second, this.abscissa);
			this.interceptAboveIntersection = checkIntersection(intercept, above, this.abscissa);

			IntersectionFinder.this.fullOrdering.remove(this.abscissa);
			//IntersectionFinder.this.fireCurrentAbscissaChanged();
		}

		public void undoCommand() {
			if (this.interceptAboveIntersection != null) {
				IntersectionFinder.this.result.remove(this.interceptAboveIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.interceptAboveIntersection.abscissa);
			}

			if (this.underSecondIntersection != null) {
				IntersectionFinder.this.result.remove(this.underSecondIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.underSecondIntersection.abscissa);
			}

			if (this.interceptUnderIntersection != null) {
				IntersectionFinder.this.result.remove(this.interceptUnderIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.interceptUnderIntersection.abscissa);
			}

			if (this.aboveSecondIntersection != null) {
				IntersectionFinder.this.result.remove(this.aboveSecondIntersection.intersection);
				IntersectionFinder.this.fullOrdering.remove(this.aboveSecondIntersection.abscissa);
			}

			IntersectionFinder.this.fullOrdering.add(this.abscissa);
			//IntersectionFinder.this.fireCurrentAbscissaChanged();
		}
	}
}
